﻿#region Copyright
/// <copyright>
/// Copyright (c) 2008 Ilia Shramko.
/// Released under the terms of the GNU General Public License.
/// </copyright>
#endregion

using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml;

using Osh.Media;

namespace Osh.Qwerty
{
	public class ChannelDatabase : IMediaLibrary
	{
		private static string MediaLibraryName = "Кверти";

		private QwertyClient _client;
		private string _path;
		private MediaTreeCollection _trees;
		private List<IMediaTree> _treeList;
		private List<IMedia> _channelList;

		internal ChannelDatabase(QwertyClient client)
		{
			_client = client;
			_treeList = new List<IMediaTree>();
			_channelList = new List<IMedia>();
			_trees = new MediaTreeCollection(_treeList);
		}

		#region Load
		public void Load(string path)
		{
			XmlDocument doc = new XmlDocument();
			doc.Load(path);

			XmlElement root = doc.SelectSingleNode("Database") as XmlElement;
			if(root == null)
				return;

			List<IMedia> channels = new List<IMedia>();
			foreach(XmlElement channelElement in root.SelectNodes("Channels/Channel"))
				channels.Add(ReadChannel(channelElement));

			List<IMediaTree> treeList = new List<IMediaTree>();
			foreach(XmlElement treeElement in root.SelectNodes("Trees/Tree"))
				treeList.Add(ReadTree(channels, treeElement));

			_trees = new MediaTreeCollection(treeList);
			_treeList = treeList;
			_channelList = channels;
			_path = path;
		}

		private IMediaTree ReadTree(List<IMedia> channels, XmlElement treeElement)
		{
			Guid id = GetValue<Guid>(treeElement, "@id", Guid.NewGuid());
			string name = GetValue<string>(treeElement, "Name");

			List<IMediaFolder> folders = new List<IMediaFolder>();
			Tree mediaTree = new Tree(this, id, name, folders);

			foreach(XmlElement folderElement in treeElement.SelectNodes("Folders/Folder"))
				folders.Add(ReadFolder(channels, mediaTree, folderElement));

			return mediaTree;
		}

		private Folder ReadFolder(List<IMedia> channels, MediaTree mediaTree, XmlElement folderElement)
		{
			Guid id = GetValue<Guid>(folderElement, "@id", Guid.NewGuid());
			int? serverId = GetValue<int?>(folderElement, "ServerId", null);
			string name = GetValue<string>(folderElement, "Name");

			List<IMedia> channelRefs = new List<IMedia>();
			foreach(XmlElement channelElement in folderElement.SelectNodes("Channels/Channel"))
			{
				Channel channel = ReadChannelRef(channels, channelElement);
				if(channel != null)
					channelRefs.Add(channel);
			}

			return new Folder(mediaTree, id, serverId, name, channelRefs);
		}

		private Channel ReadChannelRef(List<IMedia> channels, XmlElement channelElement)
		{
			Guid id = GetValue<Guid>(channelElement, "@id");
			foreach(Channel channel in channels)
				if(channel.Id == id)
					return channel;
			return null;
		}

		private Channel ReadChannel(XmlElement channelElement)
		{
			Guid id = GetValue<Guid>(channelElement, "@id");
			int? serverId = GetValue<int?>(channelElement, "ServerId", null);
			Uri url = GetValue<Uri>(channelElement, "Url");
			string name = GetValue<string>(channelElement, "Name");
			string description = GetValue<string>(channelElement, "Description", null);
			string logoId = GetValue<string>(channelElement, "Logo", null);
			int? number = GetValue<int?>(channelElement, "Number", null);
			int? audioTrack = GetValue<int?>(channelElement, "AudioTrack", null);
			int? volumeLevel = GetValue<int?>(channelElement, "VolumeLevel", null);

			Channel channel = new Channel(this, id, url, serverId, null, name, description,
				logoId, number, audioTrack, volumeLevel);
			return channel;
		}

		private T GetValue<T>(XmlNode parent, string name)
		{
			XmlNode node = parent.SelectSingleNode(name);
			if(node != null && (node is XmlElement || node is XmlAttribute))
			{
				string value;
				if(node is XmlElement)
					value = node.InnerText;
				else
					value = node.Value;

				if(value.Length > 0)
					return ConvertValue<T>(value);
			}
			throw new ArgumentOutOfRangeException("name", node != null ? node.OuterXml : "", "Invalid property value.");
		}

		private T GetValue<T>(XmlNode parent, string name, T defaultValue)
		{
			XmlNode node = parent.SelectSingleNode(name);
			if(node == null)
				return defaultValue;

			string value;
			if(node is XmlElement)
				value = node.InnerText;
			else if(node is XmlAttribute)
				value = node.Value;
			else
				return defaultValue;

			if(value.Length == 0)
				return defaultValue;

			return ConvertValue<T>(value);
		}

		private T ConvertValue<T>(string value)
		{
			Type type = typeof(T);
			Type baseType = type;
			if(type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
			{
				if(string.IsNullOrEmpty(value))
					return (T)(object)null;
				baseType = type.GetGenericArguments()[0];
			}

			if(baseType == typeof(Uri))
				return (T)(object)new Uri(value);
			else if(baseType == typeof(Guid))
				return (T)(object)new Guid(value);
			else if(baseType == typeof(DateTime))
				return (T)(object)XmlConvert.ToDateTime(value, XmlDateTimeSerializationMode.Utc);
			else
				return (T)Convert.ChangeType(value, baseType);
		}
		#endregion Load

		#region Save
		public void Save()
		{
			Save(_path);
		}

		/// <summary>
		/// 
		/// </summary>
		/// <remarks>
		/// Invisible in COM for security reasons.
		/// </remarks>
		/// <param name="path"></param>
		[ComVisible(false)]
		public void Save(string path)
		{
			XmlDocument doc = new XmlDocument();
			doc.XmlResolver = null;
			doc.PreserveWhitespace = false; // To allow xml formatting when it's being saved

			doc.AppendChild(doc.CreateXmlDeclaration("1.0", "utf-8", "yes"));

			XmlElement root = (XmlElement)doc.AppendChild(doc.CreateElement("Database"));

			XmlElement treesElement = AddElement(root, "Trees");
			foreach(Tree tree in _treeList)
				WriteTree(treesElement, tree);

			XmlElement channelsElement = AddElement(root, "Channels");
			foreach(Channel channel in _channelList)
				WriteChannel(channelsElement, channel);

			if(!File.Exists(path))
			{
				string folder = Path.GetDirectoryName(path);
				if(!string.IsNullOrEmpty(folder) && !Directory.Exists(folder))
					Directory.CreateDirectory(folder);
			}
			doc.Save(path);
			_path = path;
		}

		private void WriteTree(XmlElement treesElement, Tree tree)
		{
			XmlElement treeElement = AddElement(treesElement, "Tree");
			treeElement.SetAttribute("id", tree.Id);
			AddElement(treeElement, "Name", tree.Name);

			XmlElement foldersElement = AddElement(treeElement, "Folders");
			foreach(Folder folder in tree.Folders)
				WriteFolder(foldersElement, folder);
		}
		
		private void WriteFolder(XmlElement foldersElement, Folder folder)
		{
			XmlElement folderElement = AddElement(foldersElement, "Folder");
			folderElement.SetAttribute("id", folder.Id);
			if(folder.ServerId != null)
				AddElement(folderElement, "ServerId", folder.ServerId);
			AddElement(folderElement, "Name", folder.Name);

			XmlElement channelsElement = AddElement(folderElement, "Channels");
			foreach(Channel channel in folder.Channels)
				WriteChannelRef(channelsElement, channel);
		}

		private void WriteChannelRef(XmlElement channelsElement, Channel channel)
		{
			XmlElement channelElement = AddElement(channelsElement, "Channel");
			channelElement.SetAttribute("id", channel.Id.ToString());
		}

		private void WriteChannel(XmlElement channelsElement, Channel channel)
		{
			XmlElement channelElement = AddElement(channelsElement, "Channel");
			channelElement.SetAttribute("id", channel.Id.ToString());
			AddElement(channelElement, "ServerId", channel.ServerId);
			AddElement(channelElement, "Url", channel.Attributes[MediaAttributeNames.SourceUrl]);
			AddElement(channelElement, "Name", channel.Name);
			AddElement(channelElement, "Description", channel.Description);
			AddElement(channelElement, "Logo", channel.LogoId);
			if(channel.ChannelNumber != null)
				AddElement(channelElement, "Number", channel.ChannelNumber);
			if(channel.AudioIndex != null)
				AddElement(channelElement, "AudioIndex", channel.AudioIndex);
			if(channel.VolumeLevel != null)
				AddElement(channelElement, "VolumeLevel", channel.VolumeLevel);
		}

		private XmlElement AddElement(XmlElement parent, string name)
		{
			return (XmlElement)parent.AppendChild(parent.OwnerDocument.CreateElement(name));
		}

		private XmlElement AddElement<T>(XmlElement parent, string name, T value)
		{
			string text;
			if(typeof(T) == typeof(DateTime))
				text = XmlConvert.ToString((DateTime)(object)value, XmlDateTimeSerializationMode.Utc);
			else
				text = Convert.ToString((object)value);

			XmlElement element = (XmlElement)parent.AppendChild(parent.OwnerDocument.CreateElement(name));
			element.InnerText = text;
			return element;
		}
		#endregion Save

		internal int? GetChannelServerIdBySourceUrl(Uri url)
		{
			foreach(Channel channel in _channelList)
				if(channel.SourceUrl == url)
					return channel.ServerId;
			return null;
		}

		internal bool TryGetChannelByServerId(int serverId, out Channel channel)
		{
			foreach(Channel c in _channelList)
				if(c.ServerId == serverId)
				{
					channel = c;
					return true;
				}
			channel = null;
			return false;
		}

		internal void Update(ChannelDatabase newDatabase)
		{
			// Updates existing and adds new channels
			foreach(Channel newChannel in newDatabase._channelList)
			{
				if(newChannel.ServerId == null)
					continue;

				List<Channel> channels = GetChannelsByServerId(newChannel.ServerId.Value);
				if(channels.Count > 0)
				{
					foreach(Channel channel in channels)
						channel.Update(newChannel);
				}
				else
					_channelList.Add(newChannel);
			}

			// Removes deleted channels
			for(int i = 0; i < _channelList.Count; i++)
			{
				Channel channel = (Channel)_channelList[i];
				if(channel.ServerId == null)
					continue;

				List<Channel> newChannels = newDatabase.GetChannelsByServerId(channel.ServerId.Value);
				if(newChannels.Count == 0)
				{
					RemoveChannel(channel);
					i--;
				}
			}

			// Updates existing and adds new trees
			foreach(Tree newTree in newDatabase._treeList)
			{
				Tree tree;
				if(TryGetTree(newTree.Guid, out tree))
					tree.Update(newTree);
				else
					_treeList.Add(newTree);
			}
		}

		private bool TryGetTree(Guid treeId, out Tree tree)
		{
			foreach(Tree t in _treeList)
			{
				if(t.Guid == treeId)
				{
					tree = t;
					return true;
				}
			}
			tree = null;
			return false;
		}

		private List<Channel> GetChannelsByServerId(int serverId)
		{
			List<Channel> channels = new List<Channel>();
			foreach(Channel channel in _channelList)
				if(channel.ServerId == serverId)
					channels.Add(channel);
			return channels;
		}

		private void RemoveChannel(Channel channel)
		{
			_channelList.Remove(channel);
			foreach(Tree tree in _treeList)
				tree.RemoveChannel(channel);
		}

		internal QwertyClient Client
		{
			get { return _client; }
		}

		internal List<IMedia> Channels
		{
			get { return _channelList; }
		}

		internal List<IMediaTree> Trees
		{
			get { return _treeList; }
		}

		public bool TryGetMedia(Guid id, out IMedia media)
		{
			foreach(Channel channel in _channelList)
			{
				if(channel.Id == id)
				{
					media = channel;
					return true;
				}
			}
			media = null;
			return false;
		}

		public bool TryGetMedia(Uri mediaUri, out IMedia media)
		{
			foreach(Channel channel in _channelList)
			{
				if(channel.Uri == mediaUri)
				{
					media = channel;
					return true;
				}
			}
			media = null;
			return false;
		}

		#region IMediaLibrary Members
		public string Name
		{
			get { return MediaLibraryName; }
		}

		public Type DefaultPlayer
		{
			get { return typeof(Osh.ViewRight.ViewRightApplication); }
		}

		public IMedia GetMedia(string mediaUri)
		{
			return GetMedia(MediaBase.ParseMediaUri(mediaUri));
		}

		[ComVisible(false)]
		public IMedia GetMedia(Uri mediaUri)
		{
			IMedia media;
			if(!TryGetMedia(mediaUri, out media))
				throw new ArgumentOutOfRangeException("mediaUri",
					string.Format("Cannot find media by uri '{0}'.", mediaUri));
			return media;
		}

		public MediaTreeCollection MediaTrees
		{
			get { return _trees; }
		}
		#endregion
	}
}
